Decentralized Module Update Service Rev0

Motivation

Since https://github.com/filecoin-station/core/pull/316, Station Core is keeping its Zinnia module sources up to date by fetching most current versions on a fixed loop. It does this by asking the GitHub API for the latest tag of the module, and then fetching the release tarball if its newer than the last seen latest tag.

Operators have since complained in #station on Filecoin slack, that GitHub is inaccessible in certain locations, and they would need an update solution that doesn’t rely on GitHub.

Centralized suggestion

The first idea was to create a new service, that sits in front of the GitHub API, refreshes its state periodically, and serves the tarballs. In order not to get rate limiting problems, Cloudflare sits in front of the service and caches responses indefinitely.

This has drawbacks:

  • The update service becomes a single point of failure
  • The team needs to pay for hosting, cloudflare
  • The team needs to perform DevOps

Decentralized approach

Instead, use a smart contract for tracking references (CIDs) to the last version of a module, and store module sources on IPFS.

Publish

sequenceDiagram
  participant a as GitHub Action
  participant w as web3.storage
	participant c as Smart Contract
  a ->> w: upload module CAR
  w ->> a: return CID
  a ->> c: set(module, CID)

Update / Get Latest Version

sequenceDiagram
  participant s as Station Core
  participant c as Smart Contract
  participant w as web3.storage
  s ->> c: get(module)
  c ->> s: return CID
  s ->> w: download given CID
  w ->> s: CAR file

Components

Smart Contract

// SPDX-License-Identifier: (MIT or Apache-2.0)

import "../lib/openzeppelin-contracts/contracts/access/AccessControl.sol";
pragma solidity ^0.8.19;

contract ZinniaModules is AccessControl {
    bytes32 public constant UPDATER_ROLE = keccak256("UPDATER_ROLE");

    mapping (string => string) public modules;

    constructor(address updater) {
        _grantRole(UPDATER_ROLE, updater);
    }

    function setLatest (string memory module, string memory cid) public {
      require(hasRole(UPDATER_ROLE, msg.sender), "Not an updater");
      modules[module] = cid;
    }

    function getLatest(
        string memory module
    ) public view returns (string) {
        require(modules[module] !== "", "Module not found");
        return modules[module];
    }
}

GitHub Action

  • has a FIL/ETH account
  • configured secrets:
    • WALLET_SEED
    • GLIF_AUTH
    • WEB3_STORAGE_*
  • activates when a new git tag is created
  • downloads release tarball
  • remembers the returned CID
  • sets up a smart contract API using ethers@6 and glif
  • reads repository name as module
  • calls contract.setLatest(module, CID) (costs gas)
  • account balance will be monitored in Grafana

Station Core

  • Uses ethers@6 / GLIF to call contract.getLatest(module) for every module registered
  • If the returned CID is different from the last seen one, update using the web3.storage public gateway (with validation)